# RawHTTPClient.py
#
# Copyright (c) 2006 Michael Hobbs
#
# This file is part of iTorrent.
#
# iTorrent is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# iTorrent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with iTorrent; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import re
from email.Parser import HeaderParser

from version import VERSION
import Resolver


URL_RE = re.compile(r'http://(.+?)(:\d+)?(/.*)')
PROXY_RE = re.compile(r'http://([^/:]+)(:\d+)?$')
STATUS_RE = re.compile(r'HTTP/\d+\.\d+ (\d{3}) (.*?)\r\n')
CHUNK_RE = re.compile(r'(\r\n)?([0-9a-f]+)[ \t]*(;.*?)?\r\n', re.IGNORECASE)
LAST_CHUNK_RE = re.compile(r'0+[ \t]*(;.*?)?\r\n')

CONNECT = 0
DATA = 1
CLOSED = 2
FAILURE = -1


# TODO: use proxy specified in registry, as is done in urllib.
def get(url, proxy, rawServer, callback, *args, **kwargs):
	client = Client(url, proxy, rawServer, callback, args, kwargs)
	client.get()
	return client


class Client(object):
	headers = {
			'User-agent': 'iTorrent/%s (RawHTTPClient)' % VERSION,
			'Connection': 'close',
			}

	def __init__(self, url, proxy, rawServer, callback, args, kwargs):
		super(Client, self).__init__()
		self.url = url
		self.proxy = proxy
		self.rawServer = rawServer
		self.callback = callback
		self.args = args
		self.kwargs = kwargs
		match = URL_RE.match(url)
		assert match
		self.address, port, self.path = match.groups()
		if port:
			self.host = self.address + port
			self.port = int(port[1:])
		else:
			self.host = self.address
			self.port = 80
		if proxy:
			self.path = url
			match = PROXY_RE.match(proxy)
			assert match
			self.address, port = match.groups()
			if port:
				self.port = int(port[1:])
			else:
				self.port = 80
			# end if
		self.socket = None
		self.closed = False
		self.readHeaders = ''
		self.chunkSize = -1
		self.chunkBuffer = ''

	def get(self):
		def startConnect(ipAddress):
			self.rawServer.async_start_connection((ipAddress, self.port), self)
		Resolver.resolve(self.address, startConnect)

	def close(self):
		if self.socket:
			self.socket.close()
			return
		self.closed = True

	def connection_started(self, socket):
		if self.closed:
			socket.close()
			return
		self.socket = socket
		headers = Client.headers.copy()
		headers['Host'] = self.host
		socket.write('GET %s HTTP/1.1\r\n' % self.path)
		for header, value in headers.items():
			socket.write('%s: %s\r\n' % (header, value))
		socket.write('\r\n')

	def connection_failed(self, unused_address, exception):
		self.reportFailure(str(exception))

	def connection_lost(self, unused_socket):
		if self.readHeaders is not None:
			self.reportFailure('Connection lost')
		else:
			self.callback(CLOSED, None, *self.args, **self.kwargs)
		# end if

	def connection_flushed(self, socket):
		pass

	def data_came_in(self, unused_address, data):
		if self.readHeaders is None:
			self.sendData(data)
			return
		self.readHeaders += data
		end = self.readHeaders.find('\r\n\r\n')
		if end < 0:
			return
		# Position end to be after the blank line
		end += 4
		self.processResponse(end)

	def processResponse(self, end):
		match = STATUS_RE.match(self.readHeaders)
		if not match:
			self.reportFailure('Invalid response')
			return
		code, reason = match.groups()
		if code != '200':
			if not self.processRedirect(code, match, end):
				self.reportFailure('Bad response: %s %s' % (code, reason))
			return
		parser = HeaderParser()
		message = parser.parsestr(self.readHeaders[match.end():end], True)
		if not self.callback(CONNECT, message, *self.args, **self.kwargs):
			self.socket.close()
			return
		data = self.readHeaders[end:]
		self.readHeaders = None
		# TODO: Error on unknown transfer-encoding
		if 'Transfer-Encoding' in message and \
				message['Transfer-Encoding'] == 'chunked':
			self.chunkSize = 0
		self.sendData(data)

	def processRedirect(self, code, match, end):
		# TODO: detect redirect loops
		# TODO: permanently change URL on 301?
		if code not in ['301', '302', '303', '307']:
			return False
		parser = HeaderParser()
		message = parser.parsestr(self.readHeaders[match.end():end], True)
		if 'Location' not in message:
			return False
		location = message['Location']
		self.socket.close()
		self.__init__(location,self.proxy,self.rawServer,self.callback,self.args,self.kwargs)
		self.get()
		return True

	def unChunk(self, data):
		if self.chunkSize < 0:
			return data
		dataLen = len(data)
		if dataLen < self.chunkSize:
			self.chunkSize -= dataLen
			return data
		chunkData = data[:self.chunkSize]
		self.chunkBuffer += data[self.chunkSize:]
		self.chunkSize = 0
		if LAST_CHUNK_RE.match(self.chunkBuffer):
			return chunkData
		match = CHUNK_RE.match(self.chunkBuffer)
		if not match:
			return chunkData
		self.chunkSize = int(match.group(2), 16)
		data = self.chunkBuffer[match.end():]
		self.chunkBuffer = ''
		return chunkData + self.unChunk(data)

	def reportFailure(self, cause):
		self.callback(FAILURE, cause, *self.args, **self.kwargs)
		if self.socket and self.socket.socket:
			self.socket.close()
		# end if

	def sendData(self, data):
		data = self.unChunk(data)
		if data and not self.callback(DATA, data, *self.args, **self.kwargs):
			self.socket.close()
		return

